// --------------------------------------------------------------------------------------------------------------------
// <copyright file="FIX42FeedPlugin.FIX.cs" company="Open Trader">
//   Copyright (c) David Denis (david.denis@systemathics.com)
// </copyright>
// <summary>
//   |  Open Trader - The Open Source Systematic Trading Platform
//   |
//   |  This program is free software: you can redistribute it and/or modify
//   |  it under the terms of the GNU General Public License as published by
//   |  the Free Software Foundation, either version 2 of the License, or
//   |  (at your option) any later version.
//   |
//   |  This program is distributed in the hope that it will be useful,
//   |  but WITHOUT ANY WARRANTY; without even the implied warranty of
//   |  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   |  GNU General Public License for more details.
//   |
//   |  You should have received a copy of the GNU General Public License
//   |  along with this program.  If not, see http://www.gnu.org/licenses
//   |
//   |  Up to date informations about Open Trader can be found at :
//   |    http://opentrader.org
//   |    http://opentrader.codeplex.com
//   |
//   |  For professional services, please visit us at :
//   |    http://www.systemathics.com
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Org.OpenTrader.Framework.Forge.Plugins
{
    #region Using Directives

    using System;
    using System.Collections.Generic;
    using System.Linq;

    using Org.OpenTrader.Framework.Forge.Interfaces;

    using QuickFix;

    using QuickFix42;

    using Message = QuickFix.Message;

    #endregion

    /// <summary>
    /// FIX42FeedPlugin class
    /// </summary>
    public partial class FIX42FeedPlugin
    {
        #region Methods

        /// <summary>
        /// The start subscription.
        /// </summary>
        /// <param name="symbol">
        /// The symbol.
        /// </param>
        /// <param name="key">
        /// The associated key.
        /// </param>
        /// <param name="depth">
        /// The depth.
        /// </param>
        /// <returns>
        /// The request ID of this start subscription request
        /// </returns>
        private MDReqID StartSubscription(string symbol, string key, int depth)
        {
            // Id of the FIX request
            MDReqID id;
            try
            {
                // Subscription request
                id = new MDReqID(String.Format("{0};{1}", symbol, key));

                var mdr = new MarketDataRequest(id, new SubscriptionRequestType(SubscriptionRequestType.SNAPSHOT_PLUS_UPDATES), new MarketDepth(depth));
                    
                    // Use parameterized instrument depth

                // Type of refresh
                mdr.set(new MDUpdateType(MDUpdateType.INCREMENTAL_REFRESH));

                // Aggregated book
                mdr.set(new AggregatedBook(true));

                // Symbols group
                var rs = new MarketDataRequest.NoRelatedSym();
                rs.set(new Symbol(symbol));
                mdr.addGroup(rs);

                // Fields group
                var et = new MarketDataRequest.NoMDEntryTypes();
                et.set(new MDEntryType(MDEntryType.BID));
                mdr.addGroup(et);
                et.set(new MDEntryType(MDEntryType.OFFER));
                mdr.addGroup(et);
                et.set(new MDEntryType(MDEntryType.TRADE));
                mdr.addGroup(et);
                et.set(new MDEntryType(MDEntryType.TRADING_SESSION_VWAP_PRICE));
                mdr.addGroup(et);
                et.set(new MDEntryType(MDEntryType.TRADING_SESSION_HIGH_PRICE));
                mdr.addGroup(et);
                et.set(new MDEntryType(MDEntryType.TRADING_SESSION_LOW_PRICE));
                mdr.addGroup(et);
                et.set(new MDEntryType(MDEntryType.OPENING_PRICE));
                mdr.addGroup(et);
                et.set(new MDEntryType(MDEntryType.CLOSING_PRICE));
                mdr.addGroup(et);

                // Send request
                Session.sendToTarget(mdr, this.connection.SessionID);
            }
            catch (Exception)
            {
                id = null;
            }

            return id;
        }

        /// <summary>
        /// This is in case of some big crash while still receiving previous subscription
        /// </summary>
        /// <param name="id">
        /// Request ID
        /// </param>
        /// <param name="quickfixMessageType">
        /// The quickfixMessageType.
        /// </param>
        private void StopOrphanedSubscription(MDReqID id, Type quickfixMessageType)
        {
            Stdout.WriteLine(
                string.Format(
                    "Received a message of type {0} related to an unknown request ID={1}\nSending an unsubscribe request (workaround)", 
                    quickfixMessageType, 
                    id.ToString()));

            Devel.NoThrow(
                delegate
                    {
                        var keys = id.ToString().Split(new[] { ';' });
                        var symbol = keys[0];
                        var mid = keys[1];
                        this.StopSubscription(symbol, mid, 99);
                    });
        }

        /// <summary>
        /// The stop subscription.
        /// </summary>
        /// <param name="symbol">
        /// The symbol.
        /// </param>
        /// <param name="key">
        /// The associated key.
        /// </param>
        /// <param name="depth">
        /// The market depth.
        /// </param>
        /// <returns>
        /// The request ID of this request
        /// </returns>
        private MDReqID StopSubscription(string symbol, string key, int depth)
        {
            // Id of the FIX request
            MDReqID id;
            try
            {
                // Subscription request
                id = new MDReqID(String.Format("{0};{1}", symbol, key));

                var mdr = new MarketDataRequest(
                    id, new SubscriptionRequestType(SubscriptionRequestType.DISABLE_PREVIOUS_SNAPSHOT_PLUS_UPDATE_REQUEST), new MarketDepth(depth));
                    
                    // Use parameterized instrument depth

                // Type of refresh
                mdr.set(new MDUpdateType(MDUpdateType.INCREMENTAL_REFRESH));

                // Aggregated book
                mdr.set(new AggregatedBook(true));

                // Symbols group
                var rs = new MarketDataRequest.NoRelatedSym();
                rs.set(new Symbol(symbol));
                mdr.addGroup(rs);

                // Fields group
                var et = new MarketDataRequest.NoMDEntryTypes();
                et.set(new MDEntryType(MDEntryType.BID));
                mdr.addGroup(et);
                et.set(new MDEntryType(MDEntryType.OFFER));
                mdr.addGroup(et);
                et.set(new MDEntryType(MDEntryType.TRADE));
                mdr.addGroup(et);
                et.set(new MDEntryType(MDEntryType.TRADING_SESSION_VWAP_PRICE));
                mdr.addGroup(et);
                et.set(new MDEntryType(MDEntryType.TRADING_SESSION_HIGH_PRICE));
                mdr.addGroup(et);
                et.set(new MDEntryType(MDEntryType.TRADING_SESSION_LOW_PRICE));
                mdr.addGroup(et);
                et.set(new MDEntryType(MDEntryType.OPENING_PRICE));
                mdr.addGroup(et);
                et.set(new MDEntryType(MDEntryType.CLOSING_PRICE));
                mdr.addGroup(et);

                // Send request
                Session.sendToTarget(mdr, this.connection.SessionID);
            }
            catch (Exception)
            {
                id = null;
            }

            return id;
        }

        #endregion

        /// <summary>
        /// The my application.
        /// </summary>
        internal class MyApplication : FIXApplication
        {
            #region Constants and Fields

            /// <summary>
            /// The layer.
            /// </summary>
            private FIX42FeedPlugin layer;

            #endregion

            #region Constructors and Destructors

            /// <summary>
            /// Initializes a new instance of the <see cref="MyApplication"/> class.
            /// </summary>
            /// <param name="layer">
            /// The layer.
            /// </param>
            public MyApplication(FIX42FeedPlugin layer)
            {
                this.layer = layer;
            }

            #endregion

            #region Public Methods

            /// <summary>
            /// Receiving a MarketDataSnapshotFullRefresh
            /// </summary>
            /// <param name="message">
            /// message content
            /// </param>
            /// <param name="session">
            /// associated session
            /// </param>
            public override void onMessage(MarketDataSnapshotFullRefresh message, SessionID session)
            {
                var id = new MDReqID();
                message.get(id);

                // Receiving a message whose identifier is not known
                if (!this.layer.requests.ContainsKey(id.ToString()))
                {
                    this.layer.StopOrphanedSubscription(id, message.GetType());
                    return;
                }

                // Fetch the related instrument
                var i = this.layer.requests[id.ToString()];

                // Datas
                var noMDEntries = new NoMDEntries();
                message.get(noMDEntries);

                // Decode and publish
                // TODO : layer.EventCB.RaiseEvent(layer, new MarketBookCell(i, Decode(message, noMDEntries)));
            }

            /// <summary>
            /// Receiving a MarketDataIncrementalRefresh
            /// </summary>
            /// <param name="message">
            /// message content
            /// </param>
            /// <param name="session">
            /// associated session
            /// </param>
            public override void onMessage(MarketDataIncrementalRefresh message, SessionID session)
            {
                var id = new MDReqID();
                message.get(id);

                // Receiving a message whose identifier is not know
                if (!this.layer.requests.ContainsKey(id.ToString()))
                {
                    this.layer.StopOrphanedSubscription(id, message.GetType());
                    return;
                }

                // Fetch the related instrument
                var i = this.layer.requests[id.ToString()];

                // Datas
                var noMDEntries = new NoMDEntries();
                message.get(noMDEntries);

                // Decode and publish
                // TODO : layer.EventCB.RaiseEvent(layer, new MarketBookCell(i, Decode(message, noMDEntries)));
            }

            /// <summary>
            /// Receiving a MarketDataRequestReject
            /// </summary>
            /// <param name="message">
            /// message content
            /// </param>
            /// <param name="session">
            /// associated session
            /// </param>
            public override void onMessage(MarketDataRequestReject message, SessionID session)
            {
                var id = new MDReqID();
                var reason = new MDReqRejReason();
                message.get(id);
                message.get(reason);

                // Receiving a message whose identifier is not known
                if (!this.layer.requests.ContainsKey(id.ToString()))
                {
                    return;
                }

                // Fetch the related instrument
                var i = this.layer.requests[id.ToString()];

                // remove mappings
                this.layer.requests.Remove(id.ToString());
                this.layer.requestsReverse.Remove(i);

                // Event
                // TODO : layer.EventCB.RaiseEvent(layer, new Subscription(i, reason.ToString()));
            }

            #endregion

            #region Methods

            /// <summary>
            /// Decode a message
            /// </summary>
            /// <param name="message">
            /// The input message.
            /// </param>
            /// <param name="noMDEntries">
            /// The no md entries.
            /// </param>
            /// <returns>
            /// A table of updates
            /// </returns>
            private MarketBookCell[] Decode(Message message, NoMDEntries noMDEntries)
            {
                IList<MarketBookCell> updates = new List<MarketBookCell>();

                // For all entries
                for (uint number = 1; number <= noMDEntries.getValue(); number++)
                {
                    try
                    {
                        var group = new MarketDataIncrementalRefresh.NoMDEntries();
                        var entryType = new MDEntryType();
                        var entryPx = new MDEntryPx();
                        var entrySize = new MDEntrySize();
                        var position = new MDEntryPositionNo();

                        // rt.ExtendedFields.Clear();
                        //// Theoritical price
                        // TheoriticalOpenPrice theo = new TheoriticalOpenPrice();
                        // if( message.isSetField(theo))
                        // {
                        // message.getField(theo);
                        // tp |= ETickInfo.TheoriticalOpenPrice;
                        // rt.TheoriticalOpeningPrice.Price = theo.getValue();
                        // rt.TheoriticalOpeningPrice.Stamp = Timings.Now;
                        // }
                        //// Suspension indicator
                        // SuspensionIndicator ind = new SuspensionIndicator();
                        // if( message.isSetField(ind))
                        // {
                        // message.getField(ind);
                        // tp |= ETickInfo.Extended;
                        // rt.ExtendedFields["SuspensionIndicator"] = ind.getObject();
                        // }
                        //// Trading phase
                        // TradingPhase phase = new TradingPhase();
                        // if ( message.isSetField(phase) )
                        // {
                        // message.getField(phase);
                        // tp |= ETickInfo.Extended;
                        // rt.ExtendedFields["TradingPhase"] = phase.getObject();
                        // }
                        message.getGroup(number, group);
                        group.get(entryType); // Type (bid/ask/trade)
                        group.get(entryPx); // Price

                        // Size
                        Devel.NoThrow(() => { group.get(entrySize); });

                        short depth = -1;
                        Devel.NoThrow(
                            delegate
                                {
                                    group.get(position); // Depth in [[1;n]] (FIX Side)
                                    depth = (short)(position.getValue() - 1);

                                    // Depth in [[0;n-1]] (CommandLine Side)
                                });

                        // Publish update
                        switch (entryType.getValue())
                        {
                            case QuickFix.MDEntryType.BID:
                                updates.Add(MarketBookCell.Bid(depth, Timings.Now, entryPx.getValue(), (int)entrySize.getValue(), new IContribution[] { }));
                                break;

                            case QuickFix.MDEntryType.OFFER:
                                updates.Add(MarketBookCell.Ask(depth, Timings.Now, entryPx.getValue(), (int)entrySize.getValue(), new IContribution[] { }));
                                break;

                                // case QuickFix.MDEntryType.TRADE:
                                // updates.Add( MarketBookCell.Trade(Timings.Now, entryPx.getValue(), (int)entrySize.getValue()));
                                // break;

                                // case QuickFix.MDEntryType.OPENINGPRICE:
                                // rt.Opening.Price = entryPx.getValue();
                                // rt.Opening.Stamp = Timings.Now;
                                // tp |= ETickInfo.Open;
                                // break;

                                // case QuickFix.MDEntryType.CLOSINGPRICE:
                                // rt.Closing.Price = entryPx.getValue();
                                // rt.Closing.Stamp = Timings.Now;
                                // tp |= ETickInfo.HardOrSoftClosed;
                                // break;

                                // case QuickFix.MDEntryType.TRADINGSESSIONHIGHPRICE:
                                // rt.High.Price = entryPx.getValue();
                                // rt.High.Stamp = Timings.Now;
                                // tp |= ETickInfo.High;
                                // break;

                                // case QuickFix.MDEntryType.TRADINGSESSIONLOWPRICE:
                                // rt.Low.Price = entryPx.getValue();
                                // rt.Low.Stamp = Timings.Now;
                                // tp |= ETickInfo.Low;
                                // break;

                                // case QuickFix.MDEntryType.TRADINGSESSIONVWAPPRICE:
                                // rt.VWap.Price = entryPx.getValue();
                                // rt.VWap.Stamp = Timings.Now;
                                // tp |= ETickInfo.VWap;
                                // break;
                        }
                    }
                    catch
                    {
                        // layer.RaiseFeedEvent(this, new FeedErrorEventArgs(i, "Some exception occured in decoding phase"));
                        Devel.ShouldNotOccur(this, "Bug in FIX42FeedPlugin");
                    }
                }

                return updates.ToArray();
            }

            #endregion
        }
    }
}